#include "server.h"
#include <boost/lexical_cast.hpp>
#ifdef DEBUG
#include <iostream>
#endif

#define USE_REMAIN(req)     \
    if (remain < (req)) {   \
        close(hdl);         \
        return;             \
    } else {                \
        remain -= req;      \
    }

std::string S2CHeaderBuf(S2C type) {
    return std::to_string(static_cast<S2CBase>(type));
}
bool win(const bool board[conndata::board_sz][conndata::board_sz], int row, int col) {
    int cnt = 0;
    for (int i = row; i >= 0 && board[i][col]; --i)
        ++cnt;
    for (int i = row + 1; i < conndata::board_sz && board[i][col]; ++i)
        ++cnt;
    if (cnt >= 5)
        return true;
    cnt = 0;
    for (int j = col; j >= 0 && board[row][j]; --j)
        ++cnt;
    for (int j = col + 1; j < conndata::board_sz && board[row][j]; ++j)
        ++cnt;
    if (cnt >= 5)
        return true;
    cnt = 0;
    for (int i = row, j = col; i >= 0 && j >= 0 && board[i][j]; --i, --j)
        ++cnt;
    for (int i = row + 1, j = col + 1; i < conndata::board_sz && j < conndata::board_sz && board[i][j]; ++i, ++j)
        ++cnt;
    if (cnt >= 5)
        return true;
    cnt = 0;
    for (int i = row, j = col; i < conndata::board_sz && j >= 0 && board[i][j]; ++i, --j)
        ++cnt;
    for (int i = row - 1, j = col + 1; i >= 0 && j < conndata::board_sz && board[i][j]; --i, ++j)
        ++cnt;
    if (cnt >= 5)
        return true;
    return false;
}
bool full(const bool board1[conndata::board_sz][conndata::board_sz], const bool board2[conndata::board_sz][conndata::board_sz]) {
    for (int i = 0; i < conndata::board_sz; ++i)
        for (int j = 0; j < conndata::board_sz; ++j)
            if (!board1[i][j] || !board2[i][j])
                return false;
    return true;
}
bool out_of_bound(uint32_t row, uint32_t col) {
#ifdef DEBUG
    if (row >= conndata::board_sz || col >= conndata::board_sz) {
        std::cerr << "Out of bound" << std::endl;
    }
#endif
    return row >= conndata::board_sz || col >= conndata::board_sz;
}
bool pos_available(uint32_t row, uint32_t col, const bool board1[conndata::board_sz][conndata::board_sz], const bool board2[conndata::board_sz][conndata::board_sz]) {
#ifdef DEBUG
    std::cerr << __FUNCTION__ << "(row = " << row << ", col = " << col << ")\n";
#endif
    if (out_of_bound(row, col))
        return false;
    if (board1[row][col]) {
#ifdef DEBUG
        std::cerr << "You have already filled the position\n";
#endif
        return false;
    }
    if (board2[row][col]) {
#ifdef DEBUG
        std::cerr << "The peer has already filled the position\n";
#endif
        return false;
    }
    return true;
}
gomokusrv::gomokusrv(int port) {
    server.set_access_channels(websocketpp::log::alevel::all);
    server.clear_access_channels(websocketpp::log::alevel::frame_payload);
    server.init_asio();
    server.set_open_handler(bind(&gomokusrv::OnOpen, this, ::_1));
    server.set_close_handler(bind(&gomokusrv::OnClose, this, _1));
    server.set_message_handler(bind(&gomokusrv::OnMessage, this, _1, _2));
    server.listen(port);
#ifdef DEBUG
    std::cerr << "Listening port " << port << std::endl;
#endif
    server.start_accept();
    server.run();
}
void gomokusrv::send(connection_hdl hdl, const std::string &buf) {
#if DEBUG
    std::cerr << "Sending: " << buf << std::endl;
#endif
    server.send(hdl, buf, websocketpp::frame::opcode::TEXT);
}
void gomokusrv::send_type(connection_hdl hdl, S2C type) {
    send(hdl, S2CHeaderBuf(type));
}
void gomokusrv::close(connection_hdl hdl) {
    server.close(hdl, 0, "");
    //OnClose(hdl);
}
void gomokusrv::OnOpen(connection_hdl hdl) {
    (void)hdl;
    std::cout << "connected" << std::endl;
    conn_list.emplace(hdl, conndata());
}
void gomokusrv::OnClose(connection_hdl hdl) {
    //reset waiting?
    auto it = conn_list.find(hdl);
    if (it != conn_list.end()) {
        if (!it->second.peer.expired()) {
#ifdef DEBUG
            std::cerr << "Peer win\n";
#endif
            send_type(it->second.peer, S2C::YOU_WIN);
            it->second.peer.reset();
        }
        conn_list.erase(it);
    } else {
        std::cerr << "Warning: The player has already disconnected???\n";
    }
    std::cout << "disconnected" << std::endl;
}
void gomokusrv::OnMessage(connection_hdl hdl, message_ptr msg) {
#ifdef DEBUG
    std::cerr << "Content of message: " << msg->get_payload() << std::endl;
#endif
    std::istringstream in(msg->get_payload());
#ifdef DEBUG
    std::cerr << "Reading package type\n";
#endif
    C2SBase c2s;
    in >> c2s;
    if (in.fail()) {
        close(hdl);
        return;
    }
#ifdef DEBUG
    std::cerr << in.str() << std::endl;
#endif
    switch (static_cast<C2S>(c2s)) {
    case C2S::MATCH:
        HandleMatchReq(hdl, in);
        break;
    case C2S::MY_POS:
        HandleMyPos(hdl, in);
        break;
    default:
#ifdef DEBUG
        std::cerr << "Unrecognized packet type " << c2s << std::endl;
#endif
        close(hdl);
        break;
    }
}
void gomokusrv::HandleMatchReq(connection_hdl hdl, std::istringstream& in) {
#ifdef DEBUG
    std::cerr << "Handle match request\n";
#endif
    auto& data1 = conn_list[hdl];
    if (!data1.peer.expired()) {
#ifdef DEBUG
        std::cerr << "The player starts a new game, the original peer wins\n";
#endif
        send_type(data1.peer, S2C::YOU_WIN);
        conn_list[data1.peer].peer.reset();
        data1.peer.reset();
    }
#ifdef DEBUG
    std::cerr << "Reading the user name\n";
#endif
    in >> data1.name;
    if (in.fail()) {
        close(hdl);
        return;
    }
#ifdef DEBUG
    std::cerr << "The user name is " << data1.name << std::endl;
#endif
    if (waiting.expired()) {
        waiting = hdl;
    } else {
        start_game(hdl, waiting);
        waiting.reset();
    }
}

void gomokusrv::start_game(connection_hdl player, connection_hdl peer) {
#ifdef DEBUG
    std::cerr << "Starting game\n";
#endif
    conndata &data1 = conn_list[player];
    conndata &data2 = conn_list[peer];
    data1.peer = peer;
    data2.peer = player;
    memset(data1.board, 0, sizeof(data1.board));
    memset(data2.board, 0, sizeof(data2.board));
    matched(player, data2);
    matched(peer, data1);
    // TODO: random
    send_type(player, S2C::YOUR_TURN);
    data1.myturn = true;
    data2.myturn = false;
}
void gomokusrv::matched(connection_hdl player, const conndata &peer) {
    auto buf = S2CHeaderBuf(S2C::MATCHED);
    buf += (' ' + peer.name);
    send(player, buf);
}

void gomokusrv::HandleMyPos(connection_hdl hdl, std::istringstream& in) {
#ifdef DEBUG
    std::cerr << __FUNCTION__ << std::endl;
    std::cerr << "Reading row and col\n";
#endif
    uint32_t row, col;
    in >> row >> col;
    if (in.fail()) {
        close(hdl);
        return;
    }
    conndata &data1 = conn_list[hdl];
    auto &peer = data1.peer;
    conndata &data2 = conn_list[peer];
    // TODO: Handle peer offline
    if (!data1.myturn) {
#ifdef DEBUG
        std::cerr << "Not your turn\n";
#endif
        close(hdl);
        return;
    }
    if (!pos_available(row, col, data1.board, data2.board)) {
#ifdef DEBUG
        std::cerr << "The position is not available\n";
#endif
        close(hdl);
        return;
    }
    if (data1.peer.expired()) {
#ifdef DEBUG
        std::cerr << "The peer is offline, you win\n";
#endif
        send_type(hdl, S2C::YOU_WIN);
        return;
    }
    data1.myturn = false;
    data1.board[row][col] = true;
    auto buf = S2CHeaderBuf(S2C::PEER_POS);
    buf += (' ' + std::to_string(row));
    buf += (' ' + std::to_string(col));
    send(peer, buf);
    if (win(data1.board, row, col)) {
        send_type(hdl, S2C::YOU_WIN);
        send_type(peer, S2C::YOU_FAIL);
        data1.peer.reset();
        data2.peer.reset();
        return;
    }
    if (full(data1.board, data2.board)) {
        send_type(hdl, S2C::DRAW);
        send_type(peer, S2C::DRAW);
        data1.peer.reset();
        data2.peer.reset();
        return;
    }
    data2.myturn = true;
    send_type(peer, S2C::YOUR_TURN);
}
